/*
 * Copyright 2013 JROSE
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jrose.core.jdbc.persistence.query;

import lombok.AccessLevel;
import lombok.Getter;
import org.apache.commons.lang3.Validate;
import org.jrose.core.jdbc.entity.EntityDefinition;
import org.jrose.core.jdbc.entity.field.PersistenceField;
import org.jrose.core.jdbc.entity.field.internal.RevisionField;
import org.jrose.core.jdbc.entity.field.internal.UpdateTimestampField;
import org.jrose.core.util.ConversionUtils;
import org.jrose.core.util.StringFormatter;
import org.springframework.core.convert.ConversionService;
import org.springframework.util.Assert;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public class Update<E extends Serializable> implements Serializable {
    private static final long serialVersionUID = -6622315462307175263L;
    private static final ConversionService conversionService = ConversionUtils.getConversionService();

    // 'candidates' instance will be unique, pass within linked invocations in 'set' method
    private final Map<String, Update<E>> candidates;

    @Getter(AccessLevel.PRIVATE) private String field;
    @Getter(AccessLevel.PRIVATE) private Object value;
    @Getter(AccessLevel.PRIVATE) private String operator;

    public Update() {
        candidates = new LinkedHashMap<>();
    }

    public Update(Map<String, Update<E>> candidates) {
        this.candidates = candidates;
    }

    public static <T extends Serializable> Update<T> update(String key, Object value) {
        return new Update<T>().set(key, value);
    }

    public Update<E> set(String field, Object value) {
        Validate.notNull(field, "Field must not be null");
        Validate.isTrue(!this.candidates.containsKey(field), "Field %s already exists", field);

        this.field = field;
        this.value = value;
        this.operator = "$set";
        this.candidates.put(field, this);
        return new Update<E>(this.candidates);
    }

    public Update<E> inc(String field) {
        Validate.notNull(field, "Field must not be null");
        Validate.isTrue(!this.candidates.containsKey(field), "Field %s already exists", field);

        this.field = field;
        this.operator = "$inc";
        this.candidates.put(field, this);
        return new Update<E>(this.candidates);
    }

    public Sql generateUpdateSQL(EntityDefinition<E> entityDefinition) {
        Validate.notNull(entityDefinition, "EntityDefinition must not be null");

        StringBuilder candidateSQL = new StringBuilder();
        Map<String, Object> namedParameters = new LinkedHashMap<>();

        final Map<String, AtomicInteger> parameterCounter = new HashMap<>();

        Set<String> processedFields = new HashSet<>();
        for (Update<E> update : candidates.values()) {
            String field = update.getField();
            Object value = update.getValue();
            String operator = update.getOperator();

            PersistenceField persistenceField = entityDefinition.getPersistenceField(field);
            Assert.notNull(persistenceField);
            Assert.isTrue(!persistenceField.isPrimaryKeyField());

            String column = persistenceField.getColumnName();
            switch (operator) {
                case "$set": {
                    String parameter = generateParameterName(parameterCounter, column);
                    candidateSQL.append(column).append("=:").append(parameter).append(",");
                    if (persistenceField.isEnumField() && value != null) {
                        value = conversionService.convert(value, String.class);
                    }
                    namedParameters.put(parameter, value);
                    break;
                }
                case "$inc": {
                    candidateSQL.append(column).append("=").append(column).append("+1,");
                }
                default: {
                    throw new UnsupportedOperationException();
                }
            }
            processedFields.add(field);
        }

        // automatically update @Updatetimestamp and @Revision
        long current = System.currentTimeMillis();
        for (UpdateTimestampField updateTimestampField : entityDefinition.getUpdateTimestampFields()) {
            if (processedFields.contains(updateTimestampField.getFieldName())) {
                continue;
            }
            String column = updateTimestampField.getColumnName();
            String parameter = generateParameterName(parameterCounter, column);

            candidateSQL.append(column).append("=:").append(parameter).append(",");
            namedParameters.put(parameter, conversionService.convert(current, updateTimestampField.getFieldType()));
        }
        for (RevisionField revisionField : entityDefinition.getRevisionFields()) {
            if (processedFields.contains(revisionField.getFieldName())) {
                continue;
            }
            String column = revisionField.getColumnName();
            candidateSQL.append(column).append("=").append(column).append("+1,");
        }

        return new Sql(
                Collections.singletonList(StringFormatter.trimLastComma(candidateSQL.toString())),
                namedParameters
        );
    }

    private String generateParameterName(Map<String, AtomicInteger> parameterCounter, String column) {
        if (parameterCounter.containsKey(column)) {
            int count = parameterCounter.get(column).getAndIncrement();
            return "_" + column + count;
        } else {
            parameterCounter.put(column, new AtomicInteger(0));
            return "_" + column;
        }
    }
}
